/*
 * $Id: EJBInterceptor.java 33 2010-05-08 18:27:17Z samaxes $
 *
 * Copyright 2008 samaxes.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.samaxes.stripejb3;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.controller.ExecutionContext;
import net.sourceforge.stripes.controller.Interceptor;
import net.sourceforge.stripes.controller.Intercepts;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.ReflectUtil;

/**
 * <p>
 * An {@code Interceptor} that uses a initial context to inject EJB beans into newly created action beans immediateley
 * following ActionBeanResolution.
 * </p>
 * 
 * <p>
 * To configure {@code EJBInterceptor}, add the following initialization parameters to your Stripes Filter configuration
 * in <code>web.xml</code>:
 * </p>
 * 
 * <p>
 * <strong>Stripes 1.5.x:</strong>
 * 
 * <pre>
 * &lt;init-param&gt;
 *     &lt;param-name&gt;Interceptor.Classes&lt;/param-name&gt;
 *     &lt;param-value&gt;
 *         com.samaxes.stripes.ejb3.EJBInterceptor
 *     &lt;/param-value&gt;
 * &lt;/init-param&gt;
 * </pre>
 * 
 * </p>
 * 
 * <p>
 * <strong>Stripes 1.4.x:</strong>
 * 
 * <pre>
 * &lt;init-param&gt;
 *     &lt;param-name&gt;Interceptor.Classes&lt;/param-name&gt;
 *     &lt;param-value&gt;
 *         com.samaxes.stripes.ejb3.EJBInterceptor,
 *         net.sourceforge.stripes.controller.BeforeAfterMethodInterceptor
 *     &lt;/param-value&gt;
 * &lt;/init-param&gt;
 * </pre>
 * 
 * </p>
 * 
 * <p>
 * If one or more interceptors are already configured in your <code>web.xml</code> simply separate the fully qualified
 * names of the interceptors with commas.
 * </p>
 * 
 * @see EJBBean
 * @author Samuel Santos
 * @version $Revision: 33 $
 * @deprecated Please update to the new {@link com.samaxes.stripes.ejb3.EJBInterceptor EJBInterceptor} instead.
 */
@Intercepts(LifecycleStage.ActionBeanResolution)
public class EJBInterceptor implements Interceptor {

    private static final Log log = Log.getInstance(EJBInterceptor.class);

    private static InitialContext ctx = null;

    /** Lazily filled in map of Class to methods annotated with EJBBean. */
    private static Map<Class<?>, Collection<Method>> methodMap = new ConcurrentHashMap<Class<?>, Collection<Method>>();

    /** Lazily filled in map of Class to fields annotated with EJBBean. */
    private static Map<Class<?>, Collection<Field>> fieldMap = new ConcurrentHashMap<Class<?>, Collection<Field>>();

    /**
     * Allows ActionBean resolution to proceed and then once the ActionBean has been located performs the EJB injection.
     * 
     * @param ctx the current execution context
     * @return the Resolution produced by calling context.proceed()
     * @throws Exception if the EJB binding process produced unrecoverable errors
     */
    public Resolution intercept(ExecutionContext ctx) throws Exception {
        Resolution resolution = ctx.proceed();
        log.debug("Running EJB dependency injection for instance of ", ctx.getActionBean().getClass().getSimpleName());
        ActionBean bean = ctx.getActionBean();

        // First inject any values using annotated methods
        for (Method method : getMethods(bean.getClass())) {
            try {
                EJBBean ejbBean = method.getAnnotation(EJBBean.class);
                boolean nameSupplied = !"".equals(ejbBean.value());
                String name = nameSupplied ? ejbBean.value() : methodToPropertyName(method);
                Object managedBean = findEJB(name);
                method.invoke(bean, managedBean);
            } catch (Exception e) {
                throw new StripesRuntimeException("Exception while trying to lookup and inject "
                        + "an EJB bean into a bean of type " + bean.getClass().getSimpleName() + " using method "
                        + method.toString(), e);
            }
        }

        // And then inject any properties that are annotated
        for (Field field : getFields(bean.getClass())) {
            try {
                EJBBean ejbBean = field.getAnnotation(EJBBean.class);
                boolean nameSupplied = !"".equals(ejbBean.value());
                String name = nameSupplied ? ejbBean.value() : field.getName();
                Object managedBean = findEJB(name);
                field.set(bean, managedBean);
            } catch (Exception e) {
                throw new StripesRuntimeException("Exception while trying to lookup and inject "
                        + "a EJB bean into a bean of type " + bean.getClass().getSimpleName()
                        + " using field access on field " + field.toString(), e);
            }
        }

        return resolution;
    }

    /**
     * Fetches the methods on a class that are annotated with EJBBean. The first time it is called for a particular
     * class it will introspect the class and cache the results. All non-overridden methods are examined, including
     * protected and private methods. If a method is not public an attempt it made to make it accessible - if it fails
     * it is removed from the collection and an error is logged.
     * 
     * @param clazz the class on which to look for EJBBean annotated methods
     * @return the collection of methods with the annotation
     */
    protected static Collection<Method> getMethods(Class<?> clazz) {
        Collection<Method> methods = methodMap.get(clazz);
        if (methods == null) {
            methods = ReflectUtil.getMethods(clazz);
            Iterator<Method> iterator = methods.iterator();

            while (iterator.hasNext()) {
                Method method = iterator.next();
                if (!method.isAnnotationPresent(EJBBean.class)) {
                    iterator.remove();
                } else {
                    // If the method isn't public, try to make it accessible
                    if (!method.isAccessible()) {
                        try {
                            method.setAccessible(true);
                        } catch (SecurityException se) {
                            throw new StripesRuntimeException("Method " + clazz.getName() + "." + method.getName()
                                    + "is marked " + "with @EJBBean and is not public. An attempt to call "
                                    + "setAccessible(true) resulted in a SecurityException. Please "
                                    + "either make the method public or modify your JVM security "
                                    + "policy to allow Stripes to setAccessible(true).", se);
                        }
                    }

                    // Ensure the method has only the one parameter
                    if (method.getParameterTypes().length != 1) {
                        throw new StripesRuntimeException(
                                "A method marked with @EJBBean must have exactly one parameter: "
                                        + "the bean to be injected. Method [" + method.toGenericString() + "] has "
                                        + method.getParameterTypes().length + " parameters.");
                    }
                }
            }

            methodMap.put(clazz, methods);
        }

        return methods;
    }

    /**
     * Fetches the fields on a class that are annotated with EJBBean. The first time it is called for a particular class
     * it will introspect the class and cache the results. All non-overridden fields are examined, including protected
     * and private fields. If a field is not public an attempt it made to make it accessible - if it fails it is removed
     * from the collection and an error is logged.
     * 
     * @param clazz the class on which to look for EJBBean annotated fields
     * @return the collection of methods with the annotation
     */
    protected static Collection<Field> getFields(Class<?> clazz) {
        Collection<Field> fields = fieldMap.get(clazz);
        if (fields == null) {
            fields = ReflectUtil.getFields(clazz);
            Iterator<Field> iterator = fields.iterator();

            while (iterator.hasNext()) {
                Field field = iterator.next();
                if (!field.isAnnotationPresent(EJBBean.class)) {
                    iterator.remove();
                } else if (!field.isAccessible()) {
                    // If the field isn't public, try to make it accessible
                    try {
                        field.setAccessible(true);
                    } catch (SecurityException se) {
                        throw new StripesRuntimeException("Field " + clazz.getName() + "." + field.getName()
                                + "is marked " + "with @EJBBean and is not public. An attempt to call "
                                + "setAccessible(true) resulted in a SecurityException. Please "
                                + "either make the field public, annotate a public setter instead "
                                + "or modify your JVM security policy to allow Stripes to " + "setAccessible(true).",
                                se);
                    }
                }
            }

            fieldMap.put(clazz, fields);
        }

        return fields;
    }

    /**
     * Looks up an EJB managed bean from an Initial Context.
     * 
     * @param name the name of the EJB bean to look for
     * @exception StripesRuntimeException StripesRuntimeException is thrown if it is not possible to find a matching
     *            bean in the initial context.
     */
    protected static Object findEJB(String name) {
        // Try to lookup using the name provided
        try {
            if (ctx == null) {
                ctx = new InitialContext();
            }
            Object ejb = ctx.lookup(name);
            log.debug("Found EJB bean with name [", name, "]");
            return ejb;
        } catch (NamingException e) {
            throw new StripesRuntimeException("Unable to find an EJBBean with name [" + name
                    + "] in the initial context.");
        }
    }

    /**
     * A slightly unusual, and somewhat "loose" conversion of a method name to a property name. Assumes that the name is
     * in fact a mutator for a property and will do the usual {@code setFoo} to {@code foo} conversion if the method
     * follows the normal syntax, otherwise will just return the method name.
     * 
     * @param m the method to determine the property name of
     * @return a String property name
     */
    protected static String methodToPropertyName(Method m) {
        String name = m.getName();
        if (name.startsWith("set") && name.length() > 3) {
            String ret = name.substring(3, 4).toLowerCase();
            if (name.length() > 4) {
                ret += name.substring(4);
            }
            return ret;
        } else {
            return name;
        }
    }
}
